agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH] Add pg_tablespace_avail() functions 1102+ messages / 5 participants [nested] [flat]
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2019-11-08 13:12 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2019-11-08 13:12 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size. In psql, include a new "Free" column in \db+ output. --- doc/src/sgml/func.sgml | 21 ++++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 +++++++++++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++-- src/include/catalog/pg_proc.dat | 8 +++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..3a2f47c50ec 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_bsize; /* available blocks times block size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, -- 2.47.2 --vqKhBUYnSUFh3RIa-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-01 08:52 Michael Paquier <[email protected]> 0 siblings, 1 reply; 1102+ messages in thread From: Michael Paquier @ 2020-05-01 08:52 UTC (permalink / raw) To: Victor Wagner <[email protected]>; +Cc: pgsql-hackers On Thu, Apr 30, 2020 at 03:06:08PM +0300, Victor Wagner wrote: > Fix is very simple, see attach. > > Patch is made against REL_12_STABLE, but probably applicable to other > versions as well. Indeed, thanks. > my $pythonprog = "import sys;print(sys.prefix);" > . "print(str(sys.version_info[0])+str(sys.version_info[1]))"; > my $prefixcmd = > - $solution->{options}->{python} . "\\python -c \"$pythonprog\""; > + '"' . $solution->{options}->{python} . "\\python\" -c \"$pythonprog\""; > my $pyout = `$prefixcmd`; > die "Could not query for python version!\n" if $?; > my ($pyprefix, $pyver) = split(/\r?\n/, $pyout); This reminds me of ad7595b. Wouldn't it be better to use qq() here? -- Michael Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-01 09:48 Victor Wagner <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 1 reply; 1102+ messages in thread From: Victor Wagner @ 2020-05-01 09:48 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: pgsql-hackers В Fri, 1 May 2020 17:52:15 +0900 Michael Paquier <[email protected]> пишет: > On Thu, Apr 30, 2020 at 03:06:08PM +0300, Victor Wagner wrote: > > Fix is very simple, see attach. > > > > Patch is made against REL_12_STABLE, but probably applicable to > > other versions as well. > > Indeed, thanks. > > > my $pythonprog = "import sys;print(sys.prefix);" > > . > > "print(str(sys.version_info[0])+str(sys.version_info[1]))"; my > > $prefixcmd = > > - $solution->{options}->{python} . "\\python -c > > \"$pythonprog\""; > > + '"' . $solution->{options}->{python} . "\\python\" > > -c \"$pythonprog\""; my $pyout = `$prefixcmd`; > > die "Could not query for python version!\n" if $?; > > my ($pyprefix, $pyver) = split(/\r?\n/, $pyout); > > This reminds me of ad7595b. Wouldn't it be better to use qq() here? Maybe. But probably original author of this code was afraid of using too long chain of ->{} in the string substitution. So, I left this style n place. Nonetheless, using qq wouldn't save us from doubling backslashes. -- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-05 06:45 Michael Paquier <[email protected]> parent: Victor Wagner <[email protected]> 0 siblings, 1 reply; 1102+ messages in thread From: Michael Paquier @ 2020-05-05 06:45 UTC (permalink / raw) To: Victor Wagner <[email protected]>; +Cc: pgsql-hackers On Fri, May 01, 2020 at 12:48:17PM +0300, Victor Wagner wrote: > Maybe. But probably original author of this code was afraid of using > too long chain of ->{} in the string substitution. > > So, I left this style n place. > > Nonetheless, using qq wouldn't save us from doubling backslashes. Looking at this part in more details, I find the attached much more readable. I have been able to test it on my own Windows environment and the problem gets fixed (I have reproduced the original problem as well). -- Michael Attachments: [text/x-diff] python_space_dir-v2.patch (619B, ../../[email protected]/2-python_space_dir-v2.patch) download | inline diff: diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 72a21dbd41..6daa18f70e 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -498,7 +498,7 @@ sub mkvcbuild my $pythonprog = "import sys;print(sys.prefix);" . "print(str(sys.version_info[0])+str(sys.version_info[1]))"; my $prefixcmd = - $solution->{options}->{python} . "\\python -c \"$pythonprog\""; + qq("$solution->{options}->{python}\\python" -c "$pythonprog"); my $pyout = `$prefixcmd`; die "Could not query for python version!\n" if $?; my ($pyprefix, $pyver) = split(/\r?\n/, $pyout); [application/pgp-signature] signature.asc (833B, ../../[email protected]/3-signature.asc) download ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-05 07:16 Victor Wagner <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 1 reply; 1102+ messages in thread From: Victor Wagner @ 2020-05-05 07:16 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: pgsql-hackers В Tue, 5 May 2020 15:45:48 +0900 Michael Paquier <[email protected]> пишет: > On Fri, May 01, 2020 at 12:48:17PM +0300, Victor Wagner wrote: > > Maybe. But probably original author of this code was afraid of using > > too long chain of ->{} in the string substitution. > > > > So, I left this style n place. > > > > Nonetheless, using qq wouldn't save us from doubling backslashes. > > Looking at this part in more details, I find the attached much more > readable. I have been able to test it on my own Windows environment I agree, it is better. > and the problem gets fixed (I have reproduced the original problem as > well). -- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 12:53 Michael Paquier <[email protected]> parent: Victor Wagner <[email protected]> 0 siblings, 1 reply; 1102+ messages in thread From: Michael Paquier @ 2020-05-06 12:53 UTC (permalink / raw) To: Victor Wagner <[email protected]>; +Cc: pgsql-hackers On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: > I agree, it is better. Thanks, applied and back-patched down to 9.5. Now for the second problem of this thread.. -- Michael Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 13:21 Ranier Vilela <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 2 replies; 1102+ messages in thread From: Ranier Vilela @ 2020-05-06 13:21 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Victor Wagner <[email protected]>; pgsql-hackers Em qua., 6 de mai. de 2020 Ã s 09:53, Michael Paquier <[email protected]> escreveu: > On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: > > I agree, it is better. > > Thanks, applied and back-patched down to 9.5. Now for the second > problem of this thread.. > Sorry, no clue yet. I hacked the perl sources, to hardcoded perl, bison and flex with path.It works like this. For some reason, which I haven't yet discovered, msbuild is ignoring the path, where perl and bison and flex are. Although it is being set, within the 64-bit compilation environment of msvc 2019. I'm still investigating. regards, Ranier Vilela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 13:25 Ranier Vilela <[email protected]> parent: Ranier Vilela <[email protected]> 1 sibling, 1 reply; 1102+ messages in thread From: Ranier Vilela @ 2020-05-06 13:25 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Victor Wagner <[email protected]>; pgsql-hackers Em qua., 6 de mai. de 2020 Ã s 10:21, Ranier Vilela <[email protected]> escreveu: > Em qua., 6 de mai. de 2020 Ã s 09:53, Michael Paquier <[email protected]> > escreveu: > >> On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: >> > I agree, it is better. >> >> Thanks, applied and back-patched down to 9.5. Now for the second >> problem of this thread.. >> > Sorry, no clue yet. > I hacked the perl sources, to hardcoded perl, bison and flex with path.It > works like this. > For some reason, which I haven't yet discovered, msbuild is ignoring the > path, where perl and bison and flex are. > Although it is being set, within the 64-bit compilation environment of > msvc 2019. > I'm still investigating. > In fact perl, it is found, otherwise, neither build.pl would be working. But within the perl environment, when the system call is made, in this case, neither perl, bison, nor flex is found. regards, Ranier Vilela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 13:33 Ranier Vilela <[email protected]> parent: Ranier Vilela <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Ranier Vilela @ 2020-05-06 13:33 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Victor Wagner <[email protected]>; pgsql-hackers Em qua., 6 de mai. de 2020 Ã s 10:25, Ranier Vilela <[email protected]> escreveu: > Em qua., 6 de mai. de 2020 Ã s 10:21, Ranier Vilela <[email protected]> > escreveu: > >> Em qua., 6 de mai. de 2020 Ã s 09:53, Michael Paquier <[email protected]> >> escreveu: >> >>> On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: >>> > I agree, it is better. >>> >>> Thanks, applied and back-patched down to 9.5. Now for the second >>> problem of this thread.. >>> >> Sorry, no clue yet. >> I hacked the perl sources, to hardcoded perl, bison and flex with path.It >> works like this. >> For some reason, which I haven't yet discovered, msbuild is ignoring the >> path, where perl and bison and flex are. >> Although it is being set, within the 64-bit compilation environment of >> msvc 2019. >> I'm still investigating. >> > In fact perl, it is found, otherwise, neither build.pl would be working. > But within the perl environment, when the system call is made, in this > case, neither perl, bison, nor flex is found. > I'm using it like this, for now. File pgbison.pl: system("c:\\bin\\bison $headerflag $input -o $output"); File pgflex.pl: system("c:\\bin\\flex $flexflags -o$output $input"); system("c:\\perl\\bin\\perl src\\tools\\fix-old-flex-code.pl $output"); File Solution.pm: system( system('perl generate-lwlocknames.pl lwlocknames.txt'); system( system( system( system( system( system( system("perl create_help.pl ../../../doc/src/sgml/ref sql_help"); system( system( system( system( system( system('perl parse.pl < ../../../backend/parser/gram.y > preproc.y'); system( C:\dll\postgres\src\tools\msvc>\bin\grep bison *pm File MSBuildProject.pm: <Message Condition="'\$(Configuration)|\$(Platform)'=='Debug|$self->{platform}'">Running bison on $grammarFile</Message> <Command Condition="'\$(Configuration)|\$(Platform)'=='Debug|$self->{platform}'">c:\\perl\\bin\\perl "src\\tools\\msvc\\pgbison.pl" "$grammarFile"</Command> <Message Condition="'\$(Configuration)|\$(Platform)'=='Release|$self->{platform}'">Running bison on $grammarFile</Message> <Command Condition="'\$(Configuration)|\$(Platform)'=='Release|$self->{platform}'">c:\\perl\\bin\\perl "src\\tools\\msvc\\pgbison.pl" "$grammarFile"</Command> C:\dll\postgres\src\tools\msvc>\bin\grep flex *pm File MSBuildProject.pm: <Message Condition="'\$(Configuration)|\$(Platform)'=='Debug|$self->{platform}'">Running flex on $grammarFile</Message> <Command Condition="'\$(Configuration)|\$(Platform)'=='Debug|$self->{platform}'">c:\\perl\\bin\\perl "src\\tools\\msvc\\pgflex.pl" "$grammarFile"</Command> <Message Condition="'\$(Configuration)|\$(Platform)'=='Release|$self->{platform}'">Running flex on $grammarFile</Message> <Command Condition="'\$(Configuration)|\$(Platform)'=='Release|$self->{platform}'">c:\\perl\\bin\\perl "src\\tools\\msvc\\pgflex.pl" "$grammarFile"</Command> regards, Ranier Vilela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 17:14 Victor Wagner <[email protected]> parent: Ranier Vilela <[email protected]> 1 sibling, 2 replies; 1102+ messages in thread From: Victor Wagner @ 2020-05-06 17:14 UTC (permalink / raw) To: Ranier Vilela <[email protected]>; +Cc: Michael Paquier <[email protected]>; pgsql-hackers В Wed, 6 May 2020 10:21:41 -0300 Ranier Vilela <[email protected]> пишет: > Em qua., 6 de mai. de 2020 às 09:53, Michael Paquier > <[email protected]> escreveu: > > > On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: > > > I agree, it is better. > > > > Thanks, applied and back-patched down to 9.5. Now for the second > > problem of this thread.. > > > Sorry, no clue yet. > I hacked the perl sources, to hardcoded perl, bison and flex with > path.It works like this. Perl has "magic" variable $^X which expands to full path to perl executable, I wonder why build.pl doesn't use it to invoke secondary perl scripts. -- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 18:11 Andrew Dunstan <[email protected]> parent: Victor Wagner <[email protected]> 1 sibling, 1 reply; 1102+ messages in thread From: Andrew Dunstan @ 2020-05-06 18:11 UTC (permalink / raw) To: Victor Wagner <[email protected]>; Ranier Vilela <[email protected]>; +Cc: Michael Paquier <[email protected]>; pgsql-hackers On 5/6/20 1:14 PM, Victor Wagner wrote: > В Wed, 6 May 2020 10:21:41 -0300 > Ranier Vilela <[email protected]> пишет: > >> Em qua., 6 de mai. de 2020 às 09:53, Michael Paquier >> <[email protected]> escreveu: >> >>> On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: >>>> I agree, it is better. >>> Thanks, applied and back-patched down to 9.5. Now for the second >>> problem of this thread.. >>> >> Sorry, no clue yet. >> I hacked the perl sources, to hardcoded perl, bison and flex with >> path.It works like this. > Perl has "magic" variable $^X which expands to full path to perl > executable, I wonder why build.pl doesn't use it to invoke secondary > perl scripts. > We assume perl, flex and bison are in the PATH. That doesn't seem unreasonable, it's worked well for quite a long time. cheers andrew -- Andrew Dunstan https://www.2ndQuadrant.com PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 18:19 Ranier Vilela <[email protected]> parent: Victor Wagner <[email protected]> 1 sibling, 1 reply; 1102+ messages in thread From: Ranier Vilela @ 2020-05-06 18:19 UTC (permalink / raw) To: Victor Wagner <[email protected]>; +Cc: Michael Paquier <[email protected]>; pgsql-hackers Em qua., 6 de mai. de 2020 às 14:14, Victor Wagner <[email protected]> escreveu: > В Wed, 6 May 2020 10:21:41 -0300 > Ranier Vilela <[email protected]> пишет: > > > Em qua., 6 de mai. de 2020 às 09:53, Michael Paquier > > <[email protected]> escreveu: > > > > > On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: > > > > I agree, it is better. > > > > > > Thanks, applied and back-patched down to 9.5. Now for the second > > > problem of this thread.. > > > > > Sorry, no clue yet. > > I hacked the perl sources, to hardcoded perl, bison and flex with > > path.It works like this. > > Perl has "magic" variable $^X which expands to full path to perl > executable, I wonder why build.pl doesn't use it to invoke secondary > perl scripts. > I still don't think it's necessary, it was working well. My main suspicions are: 1. Path with spaces; 2. Incompatibility with < symbol, some suggest use " <Exec Command="" 3. Msbuid.exe It has been updated (version 16.5.0) 4. Perl scripts increased the level of security. 5. My user do not have administrator rights. regards, Ranier Vilela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-06 18:58 Ranier Vilela <[email protected]> parent: Ranier Vilela <[email protected]> 0 siblings, 1 reply; 1102+ messages in thread From: Ranier Vilela @ 2020-05-06 18:58 UTC (permalink / raw) To: Victor Wagner <[email protected]>; +Cc: Michael Paquier <[email protected]>; pgsql-hackers Em qua., 6 de mai. de 2020 às 15:19, Ranier Vilela <[email protected]> escreveu: > Em qua., 6 de mai. de 2020 às 14:14, Victor Wagner <[email protected]> > escreveu: > >> В Wed, 6 May 2020 10:21:41 -0300 >> Ranier Vilela <[email protected]> пишет: >> >> > Em qua., 6 de mai. de 2020 às 09:53, Michael Paquier >> > <[email protected]> escreveu: >> > >> > > On Tue, May 05, 2020 at 10:16:23AM +0300, Victor Wagner wrote: >> > > > I agree, it is better. >> > > >> > > Thanks, applied and back-patched down to 9.5. Now for the second >> > > problem of this thread.. >> > > >> > Sorry, no clue yet. >> > I hacked the perl sources, to hardcoded perl, bison and flex with >> > path.It works like this. >> >> Perl has "magic" variable $^X which expands to full path to perl >> executable, I wonder why build.pl doesn't use it to invoke secondary >> perl scripts. >> > I still don't think it's necessary, it was working well. > My main suspicions are: > 1. Path with spaces; > 2. Incompatibility with < symbol, some suggest use " > > <Exec Command="" > > 3. Msbuid.exe It has been updated (version 16.5.0) > 4. Perl scripts increased the level of security. > 5. My user do not have administrator rights. > Cause found. How it worked before 1. Call link from menu Visual Studio 2019: Auxiliary\Build\vcvars64.bat That create a console with settings to compile on 64 bits. 2. Adjusting the path manually set path=%path%;c:\perl\bin;c:\bin 3. Call build.bat Hacking pgbison.pl, to print PATH, shows that the path inside pgbison.pl, returned to being the original, without the addition of c:\perl\bin;c:\bin. my $out = $ENV{PATH}; print "Path after system call=$out\n"; Path after system call=...C:\Users\ranier\AppData\Local\Microsoft\WindowsApps;; The final part lacks: c:\perl\bin;c:\bin Now I need to find out why the path is being reset, within the perl scripts. Cause: PATH being reset. regards, Ranier Vilela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-07 00:08 Michael Paquier <[email protected]> parent: Andrew Dunstan <[email protected]> 0 siblings, 1 reply; 1102+ messages in thread From: Michael Paquier @ 2020-05-07 00:08 UTC (permalink / raw) To: Andrew Dunstan <[email protected]>; +Cc: Victor Wagner <[email protected]>; Ranier Vilela <[email protected]>; pgsql-hackers On Wed, May 06, 2020 at 02:11:34PM -0400, Andrew Dunstan wrote: > We assume perl, flex and bison are in the PATH. That doesn't seem > unreasonable, it's worked well for quite a long time. I recall that it is an assumption we rely on since MSVC scripts are around, and that's rather easy to configure, so it seems to me that changing things now would just introduce annoying changes for anybody (developers, maintainers) using this stuff. -- Michael Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-07 00:14 Ranier Vilela <[email protected]> parent: Michael Paquier <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Ranier Vilela @ 2020-05-07 00:14 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Andrew Dunstan <[email protected]>; Victor Wagner <[email protected]>; pgsql-hackers Em qua., 6 de mai. de 2020 Ã s 21:08, Michael Paquier <[email protected]> escreveu: > On Wed, May 06, 2020 at 02:11:34PM -0400, Andrew Dunstan wrote: > > We assume perl, flex and bison are in the PATH. That doesn't seem > > unreasonable, it's worked well for quite a long time. > > I recall that it is an assumption we rely on since MSVC scripts are > around, and that's rather easy to configure, so it seems to me that > changing things now would just introduce annoying changes for anybody > (developers, maintainers) using this stuff. > Ah yes, better to leave it as is. No problem for me, I already got around the difficulty. regards, Ranier Vilela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-07 00:14 Michael Paquier <[email protected]> parent: Ranier Vilela <[email protected]> 0 siblings, 2 replies; 1102+ messages in thread From: Michael Paquier @ 2020-05-07 00:14 UTC (permalink / raw) To: Ranier Vilela <[email protected]>; +Cc: Victor Wagner <[email protected]>; pgsql-hackers On Wed, May 06, 2020 at 03:58:15PM -0300, Ranier Vilela wrote: > Hacking pgbison.pl, to print PATH, shows that the path inside pgbison.pl, > returned to being the original, without the addition of c:\perl\bin;c:\bin. > my $out = $ENV{PATH}; > print "Path after system call=$out\n"; > Path after system > call=...C:\Users\ranier\AppData\Local\Microsoft\WindowsApps;; > The final part lacks: c:\perl\bin;c:\bin > > Now I need to find out why the path is being reset, within the perl scripts. FWIW, we have a buildfarm animal called drongo that runs with VS 2019, that uses Python, and that is now happy. One of my own machines uses VS 2019 as well and I have yet to see what you are describing here. Perhaps that's related to a difference in the version of perl you are using and the version of that any others? -- Michael Attachments: [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc) download ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-07 00:23 Ranier Vilela <[email protected]> parent: Michael Paquier <[email protected]> 1 sibling, 1 reply; 1102+ messages in thread From: Ranier Vilela @ 2020-05-07 00:23 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Victor Wagner <[email protected]>; pgsql-hackers Em qua., 6 de mai. de 2020 Ã s 21:14, Michael Paquier <[email protected]> escreveu: > On Wed, May 06, 2020 at 03:58:15PM -0300, Ranier Vilela wrote: > > Hacking pgbison.pl, to print PATH, shows that the path inside pgbison.pl > , > > returned to being the original, without the addition of > c:\perl\bin;c:\bin. > > my $out = $ENV{PATH}; > > print "Path after system call=$out\n"; > > Path after system > > call=...C:\Users\ranier\AppData\Local\Microsoft\WindowsApps;; > > The final part lacks: c:\perl\bin;c:\bin > > > > Now I need to find out why the path is being reset, within the perl > scripts. > > FWIW, we have a buildfarm animal called drongo that runs with VS 2019, > that uses Python, and that is now happy. One of my own machines uses > VS 2019 as well and I have yet to see what you are describing here. > Perhaps that's related to a difference in the version of perl you are > using and the version of that any others? > I really don't know what to say, I know very little about perl. The perl is: Win32 strawberry-perl 5.30.1.1 regards, Ranier VIlela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-07 05:04 Victor Wagner <[email protected]> parent: Michael Paquier <[email protected]> 1 sibling, 1 reply; 1102+ messages in thread From: Victor Wagner @ 2020-05-07 05:04 UTC (permalink / raw) To: Michael Paquier <[email protected]>; +Cc: Ranier Vilela <[email protected]>; pgsql-hackers В Thu, 7 May 2020 09:14:33 +0900 Michael Paquier <[email protected]> пишет: > On Wed, May 06, 2020 at 03:58:15PM -0300, Ranier Vilela wrote: > > Hacking pgbison.pl, to print PATH, shows that the path inside > > pgbison.pl, returned to being the original, without the addition of > > c:\perl\bin;c:\bin. my $out = $ENV{PATH}; > > print "Path after system call=$out\n"; > > Path after system > > call=...C:\Users\ranier\AppData\Local\Microsoft\WindowsApps;; > > The final part lacks: c:\perl\bin;c:\bin > > > > Now I need to find out why the path is being reset, within the perl > > scripts. > > FWIW, we have a buildfarm animal called drongo that runs with VS 2019, > that uses Python, and that is now happy. One of my own machines uses > VS 2019 as well and I have yet to see what you are describing here. > Perhaps that's related to a difference in the version of perl you are > using and the version of that any others? I doubt so. I have different machines with perl from 5.22 to 5.30 but none of tham exibits such weird behavoir. Perhaps problem is that Ranier calls vcvars64.bat from the menu, and then calls msbuild such way that is becames unrelated process. Obvoisly buildfarm animal doesn't use menu and then starts build process from same CMD.EXE process, that it called vcvarsall.but into. It is same in all OSes - Windows, *nix and even MS-DOS - there is no way to change environment of parent process. You can change environment of current process (and if this process is command interpreter you can do so by sourcing script into it. In windows this misleadingly called 'CALL', but it executes commands from command file in the current shell, not in subshell) you can pass enivronment to the child processes. But you can never affect environment of the parent or sibling process. The only exception is - if you know that some process would at startup read environment vars from some file or registry, you can modify this source in unrelated process. So, if you want perl in path of msbuild, started from Visual Studio, you should either first set path in CMD.EXE, then type command to start Studio from this very command window, or set path via control panel dialog (which modified registry). Later is what I usially do on machines wher I compile postgres. -- > -- > Michael ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-07 05:10 Victor Wagner <[email protected]> parent: Ranier Vilela <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Victor Wagner @ 2020-05-07 05:10 UTC (permalink / raw) To: Ranier Vilela <[email protected]>; +Cc: Michael Paquier <[email protected]>; pgsql-hackers В Wed, 6 May 2020 21:23:57 -0300 Ranier Vilela <[email protected]> пишет: > > The perl is: > Win32 strawberry-perl 5.30.1.1 > This perl would have problems when compiling PL/Perl (see my letter about week ago), but it have no problems running various build scripts for Postgres. I'm using it with MSVisualStudio 2019 and only unexpected thing I've encountered is that it comes with its own patch.exe, which doesn't like unix-style end-of-lines in patches (but OK with them in patched files). > regards, > Ranier VIlela -- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* Re: Postgres Windows build system doesn't work with python installed in Program Files @ 2020-05-07 11:32 Ranier Vilela <[email protected]> parent: Victor Wagner <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Ranier Vilela @ 2020-05-07 11:32 UTC (permalink / raw) To: Victor Wagner <[email protected]>; +Cc: Michael Paquier <[email protected]>; pgsql-hackers Em qui., 7 de mai. de 2020 às 02:04, Victor Wagner <[email protected]> escreveu: > В Thu, 7 May 2020 09:14:33 +0900 > Michael Paquier <[email protected]> пишет: > > > On Wed, May 06, 2020 at 03:58:15PM -0300, Ranier Vilela wrote: > > > Hacking pgbison.pl, to print PATH, shows that the path inside > > > pgbison.pl, returned to being the original, without the addition of > > > c:\perl\bin;c:\bin. my $out = $ENV{PATH}; > > > print "Path after system call=$out\n"; > > > Path after system > > > call=...C:\Users\ranier\AppData\Local\Microsoft\WindowsApps;; > > > The final part lacks: c:\perl\bin;c:\bin > > > > > > Now I need to find out why the path is being reset, within the perl > > > scripts. > > > > FWIW, we have a buildfarm animal called drongo that runs with VS 2019, > > that uses Python, and that is now happy. One of my own machines uses > > VS 2019 as well and I have yet to see what you are describing here. > > Perhaps that's related to a difference in the version of perl you are > > using and the version of that any others? > > > I doubt so. I have different machines with perl from 5.22 to 5.30 but > none of tham exibits such weird behavoir. > The perl is the same,when it was working ok. > > Perhaps problem is that Ranier calls vcvars64.bat from the menu, and > then calls msbuild such way that is becames unrelated process. > It also worked previously, using this same process, link menu and manual path configuration. What has changed: 1 In the environment, the python installation, which added entries to the path. 2. Perl scripts: Use perl's $/ more idiomatically commit beb2516e961490723fb1a2f193406afb3d71ea9c 3. Msbuild and others, have been updated.They are not the same ones that were working before. > > Obvoisly buildfarm animal doesn't use menu and then starts build > process from same CMD.EXE process, that it called vcvarsall.but into. > > It is same in all OSes - Windows, *nix and even MS-DOS - there is no > way to change environment of parent process. You can change environment > of current process (and if this process is command interpreter you can > do so by sourcing script into it. In windows this misleadingly called > 'CALL', but it executes commands from command file in the current > shell, not in subshell) you can pass enivronment to the child > processes. But you can never affect environment of the parent or > sibling process. > Maybe that's what is happening, calling system, perl or msbuild, would be creating a new environment, transferring the path that is configured in Windows, and not the path that is in the environment that was manually configured. > > The only exception is - if you know that some process would at startup > read environment vars from some file or registry, you can modify this > source in unrelated process. > > So, if you want perl in path of msbuild, started from Visual Studio, > you should either first set path in CMD.EXE, then type command to start > Studio from this very command window, or set path via control panel > dialog (which modified registry). Later is what I usially do on machines > wher I compile postgres. > buidfarm aninal, uses a more secure and reliable process, the path is already configured and does not change. Perhaps this is the way for me and for others. It would then remain to document, to warn that to work correctly, the path must be configured before entering the compilation environment. regards, Ranier Vilela ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v4] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 171 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..9bd8667c2d9 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --mxigUiQ2jY+Id+Rf-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func.sgml | 21 ++++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 94 ++++++++++++++++++++++++ src/bin/psql/describe.c | 11 ++- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 ++++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 163 insertions(+), 4 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad6571..0b4456ad958 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -30093,6 +30093,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index cedccc14129..9e1bec0b422 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef..30e0cb8d111 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,11 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +321,95 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false) + return -1; + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + return -1; + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e6cf468ac9e..8c52a126ac1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 180000) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe8..9d64da6bfb8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7680,6 +7680,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e5738..6709ed794df 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e2..3fcd4bb00ff 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.47.2 --65NGEM4fhwMw73YU-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
* [PATCH v5] Add pg_tablespace_avail() functions @ 2025-03-14 15:29 Christoph Berg <[email protected]> 0 siblings, 0 replies; 1102+ messages in thread From: Christoph Berg @ 2025-03-14 15:29 UTC (permalink / raw) This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function. --- doc/src/sgml/func/func-admin.sgml | 21 +++++ doc/src/sgml/ref/psql-ref.sgml | 2 +- src/backend/utils/adt/dbsize.c | 102 +++++++++++++++++++++++ src/bin/psql/describe.c | 32 +++++-- src/include/catalog/pg_proc.dat | 8 ++ src/test/regress/expected/tablespace.out | 21 +++++ src/test/regress/sql/tablespace.sql | 10 +++ 7 files changed, 189 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 72038fc835f..959b0b673ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_tablespace_avail</primary> + </indexterm> + <function>pg_tablespace_avail</function> ( <type>name</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para role="func_signature"> + <function>pg_tablespace_avail</function> ( <type>oid</type> ) + <returnvalue>bigint</returnvalue> + </para> + <para> + Returns the available disk space in the tablespace with the + specified name or OID. To use this function, you must + have <literal>CREATE</literal> privilege on the specified tablespace + or have privileges of the <literal>pg_read_all_stats</literal> role, + unless it is the default tablespace for the current database. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 7c05afd4719..1ea67b3659f 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1 If <literal>x</literal> is appended to the command name, the results are displayed in expanded mode. If <literal>+</literal> is appended to the command name, each tablespace - is listed with its associated options, on-disk size, permissions and + is listed with its associated options, on-disk size and free disk space, permissions and description. </para> </listitem> diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index cccc4a24c84..b395824ca3f 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -12,6 +12,12 @@ #include "postgres.h" #include <sys/stat.h> +#ifdef WIN32 +#include <fileapi.h> +#include <errhandlingapi.h> +#else +#include <sys/statvfs.h> +#endif #include "access/htup_details.h" #include "access/relation.h" @@ -316,6 +322,102 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS) } +/* + * Return available disk space of tablespace. Returns -1 if the tablespace + * directory cannot be found. + */ +static int64 +calculate_tablespace_avail(Oid tblspcOid) +{ + char tblspcPath[MAXPGPATH]; + AclResult aclresult; +#ifdef WIN32 + ULARGE_INTEGER lpFreeBytesAvailable; +#else + struct statvfs fst; +#endif + + /* + * User must have privileges of pg_read_all_stats or have CREATE privilege + * for target tablespace, either explicitly granted or implicitly because + * it is default for current database. + */ + if (tblspcOid != MyDatabaseTableSpace && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLESPACE, + get_tablespace_name(tblspcOid)); + } + + if (tblspcOid == DEFAULTTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "base"); + else if (tblspcOid == GLOBALTABLESPACE_OID) + snprintf(tblspcPath, MAXPGPATH, "global"); + else + snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid, + TABLESPACE_VERSION_DIRECTORY); + +#ifdef WIN32 + if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL)) + elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError()); + + return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */ +#else + if (statvfs(tblspcPath, &fst) < 0) + { + if (errno == ENOENT) + return -1; + else + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not statvfs directory \"%s\": %m", tblspcPath))); + } + + return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */ +#endif +} + +Datum +pg_tablespace_avail_oid(PG_FUNCTION_ARGS) +{ + Oid tblspcOid = PG_GETARG_OID(0); + int64 avail; + + /* + * Not needed for correctness, but avoid non-user-facing error message + * later if the tablespace doesn't exist. + */ + if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", tblspcOid)); + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + +Datum +pg_tablespace_avail_name(PG_FUNCTION_ARGS) +{ + Name tblspcName = PG_GETARG_NAME(0); + Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false); + int64 avail; + + avail = calculate_tablespace_avail(tblspcOid); + + if (avail < 0) + PG_RETURN_NULL(); + + PG_RETURN_INT64(avail); +} + + /* * calculate size of (one fork of) a relation * diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index e1449654f96..36486417a48 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -234,7 +234,7 @@ describeTablespaces(const char *pattern, bool verbose) appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" - " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", + " pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), gettext_noop("Location")); @@ -245,15 +245,34 @@ describeTablespaces(const char *pattern, bool verbose) printACLColumn(&buf, "spcacl"); appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"" - ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", gettext_noop("Options"), - gettext_noop("Size"), + gettext_noop("Size")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n" + " pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n" + " ELSE 'No Access'" + " END as \"%s\"", + gettext_noop("Free")); + appendPQExpBuffer(&buf, + ",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"", gettext_noop("Description")); } appendPQExpBufferStr(&buf, - "\nFROM pg_catalog.pg_tablespace\n"); + "\nFROM pg_catalog.pg_tablespace tblspc\n"); + if (verbose) + appendPQExpBufferStr(&buf, + "CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n" + " wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, NULL, "spcname", NULL, @@ -1008,7 +1027,8 @@ listAllDbs(const char *pattern, bool verbose) printACLColumn(&buf, "d.datacl"); if (verbose) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') OR\n" + " pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n" " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" " ELSE 'No Access'\n" " END as \"%s\"" diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fa9ae79082b..78c03ea6412 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7866,6 +7866,14 @@ descr => 'total disk space usage for the specified tablespace', proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'name', prosrc => 'pg_tablespace_size_name' }, +{ oid => '6015', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' }, +{ oid => '6016', + descr => 'disk stats for the specified tablespace', + proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8', + proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' }, { oid => '2324', descr => 'total disk space usage for the specified database', proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_database_size_oid' }, diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index f0dd25cdf0c..12a78c77e05 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; {random_page_cost=3.0} (1 row) +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty + ?column? | ?column? | pg_tablespace_size +----------+----------+-------------------- + t | t | 0 +(1 row) + +SELECT pg_tablespace_size('missing'); +ERROR: tablespace "missing" does not exist +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; + ?column? | ?column? | ?column? +----------+----------+---------- + t | t | t +(1 row) + +SELECT pg_tablespace_avail('missing'); +ERROR: tablespace "missing" does not exist -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- This returns a relative path as of an effect of allow_in_place_tablespaces, diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index c43a59e5957..91152335459 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0) -- check to see the parameter was used SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith'; +-- check size functions +SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check + pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000, + pg_tablespace_size('regress_tblspacewith'); -- empty +SELECT pg_tablespace_size('missing'); +SELECT pg_tablespace_avail('pg_default') > 1_000_000, + pg_tablespace_avail('pg_global') > 1_000_000, + pg_tablespace_avail('regress_tblspacewith') > 1_000_000; +SELECT pg_tablespace_avail('missing'); + -- drop the tablespace so we can re-use the location DROP TABLESPACE regress_tblspacewith; -- 2.53.0 --sW4eVZZF7egqrnKF-- ^ permalink raw reply [nested|flat] 1102+ messages in thread
end of thread, other threads:[~2025-03-14 15:29 UTC | newest] Thread overview: 1102+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2019-11-08 13:12 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2020-05-01 08:52 Re: Postgres Windows build system doesn't work with python installed in Program Files Michael Paquier <[email protected]> 2020-05-01 09:48 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Victor Wagner <[email protected]> 2020-05-05 06:45 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Michael Paquier <[email protected]> 2020-05-05 07:16 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Victor Wagner <[email protected]> 2020-05-06 12:53 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Michael Paquier <[email protected]> 2020-05-06 13:21 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[email protected]> 2020-05-06 13:25 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[email protected]> 2020-05-06 13:33 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[email protected]> 2020-05-06 17:14 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Victor Wagner <[email protected]> 2020-05-06 18:11 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Andrew Dunstan <[email protected]> 2020-05-07 00:08 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Michael Paquier <[email protected]> 2020-05-07 00:14 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[email protected]> 2020-05-06 18:19 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[email protected]> 2020-05-06 18:58 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[email protected]> 2020-05-07 00:14 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Michael Paquier <[email protected]> 2020-05-07 00:23 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[email protected]> 2020-05-07 05:10 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Victor Wagner <[email protected]> 2020-05-07 05:04 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Victor Wagner <[email protected]> 2020-05-07 11:32 ` Re: Postgres Windows build system doesn't work with python installed in Program Files Ranier Vilela <[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 v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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] Add pg_tablespace_avail() functions Christoph Berg <[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 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 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 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 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] Add pg_tablespace_avail() functions Christoph Berg <[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 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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] Add pg_tablespace_avail() functions Christoph 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 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 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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 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 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 v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[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 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] Add pg_tablespace_avail() functions Christoph Berg <[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 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 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph 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 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 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[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 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] Add pg_tablespace_avail() functions Christoph Berg <[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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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] Add pg_tablespace_avail() functions Christoph Berg <[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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[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 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[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 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] Add pg_tablespace_avail() functions Christoph Berg <[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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 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] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH 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 v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[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] Add pg_tablespace_avail() functions Christoph Berg <[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 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] Add pg_tablespace_avail() functions Christoph Berg <[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 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 v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph 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 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 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 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 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 v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v5] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH v4] Add pg_tablespace_avail() functions Christoph Berg <[email protected]> 2025-03-14 15:29 [PATCH] Add pg_tablespace_avail() functions Christoph Berg <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox