Skip to content

Commit 6d3ada5

Browse files
df7cbhackorum
authored andcommitted
Add pg_tablespace_avail() functions
This exposes the f_avail value from statvfs() on tablespace directories on the SQL level, allowing monitoring of free disk space from within the server. On windows, GetDiskFreeSpaceEx() is used. Permissions required match those from pg_tablespace_size(). In psql, include a new "Free" column in \db+ output. Add test coverage for pg_tablespace_avail() and the previously not covered pg_tablespace_size() function.
1 parent 44056f6 commit 6d3ada5

7 files changed

Lines changed: 195 additions & 6 deletions

File tree

doc/src/sgml/func/func-admin.sgml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,6 +1755,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
17551755
</para></entry>
17561756
</row>
17571757

1758+
<row>
1759+
<entry role="func_table_entry"><para role="func_signature">
1760+
<indexterm>
1761+
<primary>pg_tablespace_avail</primary>
1762+
</indexterm>
1763+
<function>pg_tablespace_avail</function> ( <type>name</type> )
1764+
<returnvalue>bigint</returnvalue>
1765+
</para>
1766+
<para role="func_signature">
1767+
<function>pg_tablespace_avail</function> ( <type>oid</type> )
1768+
<returnvalue>bigint</returnvalue>
1769+
</para>
1770+
<para>
1771+
Returns the available disk space in the file system hosting the tablespace with the
1772+
specified name or OID. To use this function, you must
1773+
have <literal>CREATE</literal> privilege on the specified tablespace
1774+
or have privileges of the <literal>pg_read_all_stats</literal> role,
1775+
unless it is the default tablespace for the current database.
1776+
</para></entry>
1777+
</row>
1778+
17581779
<row>
17591780
<entry role="func_table_entry"><para role="func_signature">
17601781
<indexterm>

doc/src/sgml/ref/psql-ref.sgml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,7 @@ SELECT $1 \parse stmt1
15011501
If <literal>x</literal> is appended to the command name, the results
15021502
are displayed in expanded mode.
15031503
If <literal>+</literal> is appended to the command name, each tablespace
1504-
is listed with its associated options, on-disk size, permissions and
1504+
is listed with its associated options, on-disk size and free disk space, permissions and
15051505
description.
15061506
</para>
15071507
</listitem>

src/backend/utils/adt/dbsize.c

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
#include "postgres.h"
1313

1414
#include <sys/stat.h>
15+
#ifdef WIN32
16+
#include <fileapi.h>
17+
#include <errhandlingapi.h>
18+
#else
19+
#include <sys/statvfs.h>
20+
#endif
1521

1622
#include "access/htup_details.h"
1723
#include "access/relation.h"
@@ -316,6 +322,110 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
316322
}
317323

318324

325+
/*
326+
* Return available disk space for tablespace. Returns -1 if the tablespace
327+
* directory cannot be found.
328+
*/
329+
static int64
330+
calculate_tablespace_avail(Oid tblspcOid)
331+
{
332+
char tblspcPath[MAXPGPATH];
333+
AclResult aclresult;
334+
#ifdef WIN32
335+
ULARGE_INTEGER lpFreeBytesAvailable;
336+
#else
337+
struct statvfs fst;
338+
#endif
339+
340+
/*
341+
* User must have privileges of pg_read_all_stats or have CREATE privilege
342+
* for target tablespace, either explicitly granted or implicitly because
343+
* it is default for current database.
344+
*/
345+
if (tblspcOid != MyDatabaseTableSpace &&
346+
!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
347+
{
348+
aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
349+
if (aclresult != ACLCHECK_OK)
350+
aclcheck_error(aclresult, OBJECT_TABLESPACE,
351+
get_tablespace_name(tblspcOid));
352+
}
353+
354+
if (tblspcOid == DEFAULTTABLESPACE_OID)
355+
snprintf(tblspcPath, MAXPGPATH, "base");
356+
else if (tblspcOid == GLOBALTABLESPACE_OID)
357+
snprintf(tblspcPath, MAXPGPATH, "global");
358+
else
359+
snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
360+
TABLESPACE_VERSION_DIRECTORY);
361+
362+
#ifdef WIN32
363+
if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
364+
{
365+
_dosmaperr(GetLastError());
366+
if (errno == ENOENT)
367+
return -1;
368+
else
369+
ereport(ERROR,
370+
(errcode_for_file_access(),
371+
errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
372+
}
373+
374+
return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
375+
#else
376+
if (statvfs(tblspcPath, &fst) < 0)
377+
{
378+
if (errno == ENOENT)
379+
return -1;
380+
else
381+
ereport(ERROR,
382+
(errcode_for_file_access(),
383+
errmsg("could not get free disk space in tablespace directory \"%s\": %m", tblspcPath)));
384+
}
385+
386+
return (int64) fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
387+
#endif
388+
}
389+
390+
Datum
391+
pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
392+
{
393+
Oid tblspcOid = PG_GETARG_OID(0);
394+
int64 avail;
395+
396+
/*
397+
* Not needed for correctness, but avoid non-user-facing error message
398+
* later if the tablespace doesn't exist.
399+
*/
400+
if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
401+
ereport(ERROR,
402+
errcode(ERRCODE_UNDEFINED_OBJECT),
403+
errmsg("tablespace with OID %u does not exist", tblspcOid));
404+
405+
avail = calculate_tablespace_avail(tblspcOid);
406+
407+
if (avail < 0)
408+
PG_RETURN_NULL();
409+
410+
PG_RETURN_INT64(avail);
411+
}
412+
413+
Datum
414+
pg_tablespace_avail_name(PG_FUNCTION_ARGS)
415+
{
416+
Name tblspcName = PG_GETARG_NAME(0);
417+
Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
418+
int64 avail;
419+
420+
avail = calculate_tablespace_avail(tblspcOid);
421+
422+
if (avail < 0)
423+
PG_RETURN_NULL();
424+
425+
PG_RETURN_INT64(avail);
426+
}
427+
428+
319429
/*
320430
* calculate size of (one fork of) a relation
321431
*

src/bin/psql/describe.c

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ describeTablespaces(const char *pattern, bool verbose)
224224
appendPQExpBuffer(&buf,
225225
"SELECT spcname AS \"%s\",\n"
226226
" pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n"
227-
" pg_catalog.pg_tablespace_location(oid) AS \"%s\"",
227+
" pg_catalog.pg_tablespace_location(tblspc.oid) AS \"%s\"",
228228
gettext_noop("Name"),
229229
gettext_noop("Owner"),
230230
gettext_noop("Location"));
@@ -235,15 +235,34 @@ describeTablespaces(const char *pattern, bool verbose)
235235
printACLColumn(&buf, "spcacl");
236236
appendPQExpBuffer(&buf,
237237
",\n spcoptions AS \"%s\""
238-
",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
239-
",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
238+
",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
239+
" pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
240+
" pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
241+
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))\n"
242+
" ELSE 'No Access'"
243+
" END as \"%s\"",
240244
gettext_noop("Options"),
241-
gettext_noop("Size"),
245+
gettext_noop("Size"));
246+
if (pset.sversion >= 200000)
247+
appendPQExpBuffer(&buf,
248+
",\n CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR\n"
249+
" pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR\n"
250+
" pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')\n"
251+
" THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))\n"
252+
" ELSE 'No Access'"
253+
" END as \"%s\"",
254+
gettext_noop("Free"));
255+
appendPQExpBuffer(&buf,
256+
",\n pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS \"%s\"",
242257
gettext_noop("Description"));
243258
}
244259

245260
appendPQExpBufferStr(&buf,
246-
"\nFROM pg_catalog.pg_tablespace\n");
261+
"\nFROM pg_catalog.pg_tablespace tblspc\n");
262+
if (verbose)
263+
appendPQExpBufferStr(&buf,
264+
"CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db\n"
265+
" WHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub\n");
247266

248267
if (!validateSQLNamePattern(&buf, pattern, false, false,
249268
NULL, "spcname", NULL,

src/include/catalog/pg_proc.dat

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7859,6 +7859,14 @@
78597859
descr => 'total disk space usage for the specified tablespace',
78607860
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
78617861
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
7862+
{ oid => '6015',
7863+
descr => 'free disk space for the specified tablespace',
7864+
proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
7865+
proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
7866+
{ oid => '6016',
7867+
descr => 'free disk space for the specified tablespace',
7868+
proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
7869+
proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
78627870
{ oid => '2324', descr => 'total disk space usage for the specified database',
78637871
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
78647872
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },

src/test/regress/expected/tablespace.out

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,27 @@ SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
2020
{random_page_cost=3.0}
2121
(1 row)
2222

23+
-- check size functions
24+
SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
25+
pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
26+
pg_tablespace_size('regress_tblspacewith'); -- empty
27+
?column? | ?column? | pg_tablespace_size
28+
----------+----------+--------------------
29+
t | t | 0
30+
(1 row)
31+
32+
SELECT pg_tablespace_size('missing');
33+
ERROR: tablespace "missing" does not exist
34+
SELECT pg_tablespace_avail('pg_default') > 1_000_000,
35+
pg_tablespace_avail('pg_global') > 1_000_000,
36+
pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
37+
?column? | ?column? | ?column?
38+
----------+----------+----------
39+
t | t | t
40+
(1 row)
41+
42+
SELECT pg_tablespace_avail('missing');
43+
ERROR: tablespace "missing" does not exist
2344
-- drop the tablespace so we can re-use the location
2445
DROP TABLESPACE regress_tblspacewith;
2546
-- This returns a relative path as of an effect of allow_in_place_tablespaces,

src/test/regress/sql/tablespace.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ CREATE TABLESPACE regress_tblspacewith LOCATION '' WITH (random_page_cost = 3.0)
1717
-- check to see the parameter was used
1818
SELECT spcoptions FROM pg_tablespace WHERE spcname = 'regress_tblspacewith';
1919

20+
-- check size functions
21+
SELECT pg_tablespace_size('pg_default') BETWEEN 1_000_000 and 10_000_000_000, -- rough sanity check
22+
pg_tablespace_size('pg_global') BETWEEN 100_000 and 10_000_000,
23+
pg_tablespace_size('regress_tblspacewith'); -- empty
24+
SELECT pg_tablespace_size('missing');
25+
SELECT pg_tablespace_avail('pg_default') > 1_000_000,
26+
pg_tablespace_avail('pg_global') > 1_000_000,
27+
pg_tablespace_avail('regress_tblspacewith') > 1_000_000;
28+
SELECT pg_tablespace_avail('missing');
29+
2030
-- drop the tablespace so we can re-use the location
2131
DROP TABLESPACE regress_tblspacewith;
2232

0 commit comments

Comments
 (0)